from pdb import set_trace
from pprint import pprint

class Rule(object):
	def __init__(self, updates, depends, recipe):
		self.updates = updates
		self.depends = depends
		self.recipe = recipe
		assert set(updates).isdisjoint(set(depends)), "Rule depends on itself:\n" + str(self)
	def __str__(self):
		from subprocess import list2cmdline
		return "\n\t".join([
				list2cmdline(self.updates) + " : " + list2cmdline(self.depends)
			] + map(str, self.recipe)) + "\n"
	def __repr__(self):
		return "{0}({1}, {2}, {3})".format(
			"pybake.Rule",
			self.updates,
			self.depends,
			self.recipe)
	def stale(self):
		from os.path import getmtime
		from errno import ENOENT
		try:
			upstamps = [getmtime(u) for u in self.updates]
		except OSError as exc:
			if exc.errno == ENOENT:
				return True
			raise
		try:
			depstamps = [getmtime(d) for d in self.depends]
		except OSError as exc:
			if exc.errno == ENOENT:
				return True
			raise
		return depstamps and max(depstamps) > min(upstamps)
	def __le__(self, other):
		return set(self.updates) & set(other.depends)

class Feedback(object):
	def __init__(self, msg, error=None, exit=False, done=None):
		self.msg = msg
		self.error = error
		self.exit = exit
		self.done = done

class Rules(object):
	def __init__(self):
		self.rules = set()
		self.targets = {}
		self.names = {}
	def add(self, updates, depends, recipe, name=None):
		r = Rule(updates, depends, recipe)
		for u in updates:
			assert u not in self.targets, "Rules share targets\n" + str(self.targets[u]) + str(r)
			self.targets[u] = r
		if name is not None:
			assert name not in self.names
			self.names[name] = r
		self.rules.add(r)
	def children(self, rule):
		retval = set()
		for dep in rule.depends:
			if dep in self.targets:
				retval.add(self.targets[dep])
		return retval

def make_stale(stale, options):
	threads = []
	from threading import Thread, Lock, Condition
	from Queue import Queue
	feedback_queue = Queue()
	def feedback(*pargs, **kwargs):
		feedback_queue.put(Feedback(*pargs, **kwargs))
	for job in range(1, options.jobs + 1):
		thrd = Thread(target=job_routine, name="Job %u"%(job),
			args=(feedback, stale, options))
		thrd.daemon = True
		threads.append(thrd)
		thrd.start()
	from traceback import print_exception
	from sys import stdout, stderr
	while len(threads):
		fdbk = feedback_queue.get()
		if fdbk.msg is not None:
			assert fdbk.error is not None
			strm = stderr if fdbk.error else stdout
			strm.write(fdbk.msg)
		else:
			assert fdbk.error is None
		if fdbk.exit:
			raise SystemExit(fdbk.exit)
		if fdbk.done:
			fdbk.done.join()
			threads.remove(fdbk.done)

def add_stale(rules, options, stale, target):
	if target in rules.names:
		if target in rules.targets:
			assert rules.names[target] == rules.targets[target]
		rule = rules.names[target]
	elif target in rules.targets:
		rule = rules.targets[target]
	else:
		from os.path import exists
		assert exists(target), "No rule to make %s" % target
		return False
	if rule in stale:
		return True
	for ruledep in rule.depends:
		if add_stale(rules, options, stale, ruledep):
			stale.add(rule)
	if rule not in stale and (options.always_make or rule.stale()):
		stale.add(rule)
	return rule in stale

class Stale(object):
	def __init__(self, rules, stale, jobs):
		self._rules = rules
		self._stale = stale
		self._updating = set()
		from threading import Lock, Condition, BoundedSemaphore
		self._lock = Lock()
		self._cond = Condition(self._lock)
		self._waiting = BoundedSemaphore(jobs - 1)
	def get(self):
		with self._lock:
			while True:
				for rule in self._stale:
					if not self._rules.children(rule) & (self._stale | self._updating):
						self._updating.add(rule)
						self._stale.remove(rule)
						return rule
				if not len(self._stale):
					return None
				assert self._waiting.acquire(blocking=False)
				self._cond.wait()
				self._waiting.release()
	def done(self, rule):
		with self._lock:
			self._updating.remove(rule)
			self._cond.notify_all()

def make_top(rules, options, targets):
	stale = set()
	for a in targets:
		add_stale(rules, options, stale, a)
	make_stale(Stale(rules, stale, options.jobs), options)

def make(rules, options, targets=None):
	for a in targets or ["all"]:
		if a == "all":
			subtargs = rules.targets.keys()
		elif a == "clean":
			from os import remove
			from errno import ENOENT
			for b in rules.targets.keys():
				try:
					remove(b)
				except OSError as exc:
					if exc.errno != ENOENT:
						raise
				else:
					print "Removed %r" % b
			continue
		else:
			subtargs = [a]
		make_top(rules, options, subtargs)

def job_routine(feedback, stale, options):
	try:
		from subprocess import Popen, check_call, PIPE, list2cmdline
		while True:
			rule = stale.get()
			if rule is None:
				return
			for cmd in rule.recipe:
				feedback(list2cmdline(cmd)+"\n", False)
				proc = Popen(cmd, stdout=PIPE, stderr=PIPE)
				stdoutdata, stderrdata = proc.communicate()
				feedback(stderrdata, False)
				feedback(stdoutdata, False)
				if proc.returncode:
					feedback(
						"Command '%s' returned non-zero exit status %d\n" % (
							cmd, proc.returncode),
						True, exit=True)
			if rule.stale():
				feedback(
					msg="Rule still stale after recipe executed:\n" + str(rule),
					error=True,
					exit=True)
			stale.done(rule)
	except:
		from traceback import format_exc
		feedback(format_exc(), True, exit=True)
	finally:
		from threading import current_thread
		feedback(None, done=current_thread())

def parse_args():
	from optparse import OptionParser
	parser = OptionParser()
	parser.add_option("-B", "--always-make", action="store_true")
	parser.add_option("-j", "--jobs", type="int", default=4)
	#parser.add_option("-c", "--config")
	options, args = parser.parse_args()
	return None, options, args

def cxobject(rules, source,
		includedirs=None, buildoptions=None, defines=None):
	from os.path import splitext
	obj = splitext(source)[0] + ".o"
	rules.add([obj], [source], [["gcc", "-o", obj, "-c", source] + ["-I"+a for a in includedirs or []] + ["-D"+a for a in defines or []] + (buildoptions or [])])
	return obj

def cxproject(rules, name,
		sources=None,
		sourcedir=None, # make a walk?
		#sourcedirs glob list?
		kind=None,
		includedirs=None,
		buildoptions=None,
		defines=None,
		linkoptions=None,
		objects=None,
		language=None,
		libraries=None,):
	from glob import glob
	from os.path import join
	from itertools import chain
	if sourcedir:
		if sources is None:
			sources = []
		sources += [a for a in chain(*[glob(join(sourcedir, "*."+b)) for b in ["c", "cpp", "cc", "C", "cxx"]])]
	if objects is None:
		objects = []
	objects = [cxobject(rules, a,
			includedirs=includedirs,
			buildoptions=buildoptions,
			defines=defines)
		for a in sources] + objects
	if not kind:
		kind = "console"
	if kind == "static":
		target = "lib" + name + ".a"
		rules.add([target], objects, [["ar", "crs", target] + objects])
	elif kind == "console":
		target = name
		rules.add([target], objects, [["gcc", "-o", target] + objects + ["-l"+a for a in libraries or []] + (linkoptions or [])])
	else:
		assert False, "Invalid kind specified %r" % kind
	return target

def pkg_config(options, packages):
	from shlex import split
	from subprocess import Popen, PIPE
	return split(Popen(["pkg-config"] + options + packages, stdout=PIPE).communicate()[0])
